/* CsJni - .NET / Java Bridge
 * 
 * Copyright 2010 Robert E. Burke Licensed under the Apache License, Version 
 * 2.0 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at 
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
 * law or agreed to in writing, software distributed under the License is 
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
 * KIND, either express or implied. See the License for the specific language 
 * governing permissions and limitations under the License. 
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Globalization;
using System.Diagnostics;
using System.IO;

namespace CsJni.PlatformAbstraction
{
    // See http://www.mono-project.com/Interop_with_Native_Libraries
    // http://developer.mozilla.org/en/docs/NSPR_API_Reference:Dynamic_Library_Linking
    // http://developer.apple.com/technotes/tn2005/tn2147.html#TNTAG31
    //
    //
    // UNIX equivalents
    // LoadLibrary, GetProcAddress: See http://en.wikipedia.org/wiki/Dynamic_loading
    // SearchPath: Path.GetFullPath should suffice

    internal abstract class AbstractUnixPlatform : Platform
    {
        protected AbstractUnixPlatform(JavaVMConfiguration cfg)
            : base(cfg)
        {
        }

        // (Detect Darwin/Linux)
        // http://www.koders.com/csharp/fid63F101DAACDB1442377AB51DCA436ECD47ECCBEB.aspx?s=zoom
        internal static JniPlatformId DiscoverPlatformId()
        {
            ProcessStartInfo startInfo = new ProcessStartInfo();
            startInfo.Arguments = "-s";
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            startInfo.UseShellExecute = false;
            foreach (string unameprog in new string[] { "/usr/bin/uname", "/bin/uname", "uname" })
            {
                try
                {
                    if (File.Exists(unameprog))
                    {
                        startInfo.FileName = unameprog;
                        Process uname = Process.Start(startInfo);
                        StreamReader stdout = uname.StandardOutput;
                        string output = stdout.ReadLine().Trim();
                        switch (output)
                        {
                            case "Unix": return JniPlatformId.Unix;
                            case "Linux": return JniPlatformId.Linux;
                            case "Darwin": return JniPlatformId.Mac;
                            default: return JniPlatformId.Unsupported;
                        }
                    }
                }
                catch (System.IO.FileNotFoundException)
                {
                    // The requested executable doesn't exist, try next one.
                    continue;
                }
                catch (System.ComponentModel.Win32Exception)
                {
                    continue;
                }
            }
            return JniPlatformId.Unsupported;
        }

        private VfprintfExtractor mVfprintfExtractor = null;

        public override void FaultVfPrintf()
        {
            mVfprintfExtractor = new VfprintfExtractor();
            mVfprintfExtractor.Init(this, Cfg.CrtLib);
        }

        public override string ExtractVfprintf(IntPtr filePointer, IntPtr format, IntPtr arguments)
        {
            return mVfprintfExtractor.ExtractVfprintf(filePointer, format, arguments);
        }

        public override IntPtr LoadDynamicLibrary(string fileName)
        {
            const int RTLD_LAZY = 1;
            ClearLastDLError();
            IntPtr libraryHandle = raw_dlopen(fileName, RTLD_LAZY);
            if (libraryHandle == IntPtr.Zero)
            {
                throw new JniException("dlopen failed: " + fileName + ", last error: " + GetLastDLError());
            }
            return libraryHandle;
        }

        public override void FreeDynamicLibrary(IntPtr libraryHandle)
        {
            ClearLastDLError();
            int res = raw_dlclose(libraryHandle);
            if (res != 0)
            {
                throw new JniException("dlclose failed: " + GetLastDLError());
            }
        }

        public override T GetDelegateForLibraryMethod<T>(IntPtr libraryHandle, string methodName)
        {
            ClearLastDLError();
            IntPtr proc = raw_dlsym(libraryHandle, methodName);
            if (proc == IntPtr.Zero)
            {
                string lastError = GetLastDLError();
                if (lastError == null) // meaning symbol was found but was null
                {
                    lastError = "null symbol";
                }
                throw new JniException("dlsym failed: " + methodName + ", last error: " + lastError);
            }
            return (T)(object)Marshal.GetDelegateForFunctionPointer(proc, typeof(T));
        }

        public override string FindJvmLibraryWithPath(string path)
        {
            return ExpandAndVerifyPath(path, ExpandPathType.Executable, false);
        }

        public override string LookupJvmLibrary(string version)
        {
            throw new NotSupportedException("Not supported on UNIX systems");
        }

        public override void Release()
        {
            if (mVfprintfExtractor != null)
            {
                mVfprintfExtractor.Free();
                mVfprintfExtractor = null;
            }
        }

        protected void ClearLastDLError()
        {
            GetLastDLError(); // clears any old error
        }

        protected string GetLastDLError()
        {
            IntPtr msg = raw_dlerror();
            return (msg == IntPtr.Zero) ? null : Marshal.PtrToStringAnsi(msg);
        }

        protected abstract IntPtr raw_dlopen(string fileName, int mode);

        protected abstract IntPtr raw_dlsym(IntPtr hModule, string procName);

        protected abstract IntPtr raw_dlerror();

        protected abstract int raw_dlclose(IntPtr hModule);

        private class VfprintfExtractor : AbstractVfprintfExtractor
        {
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
            private delegate int vsnprintf_Handler(
               [MarshalAs(UnmanagedType.LPStr)] StringBuilder buffer,
               int bufferSize,
               IntPtr format,
               IntPtr argList);

            private vsnprintf_Handler vsnprintf;

            public VfprintfExtractor()
            {
            }
            protected override void InitMethods()
            {
                vsnprintf = Platform.GetDelegateForLibraryMethod<vsnprintf_Handler>(LibraryHandle, "vsnprintf");
            }
            protected override void FreeMethods()
            {
                vsnprintf = null;
            }
            public override string ExtractVfprintf(IntPtr filePointer, IntPtr format, IntPtr arguments)
            {
                if (LibraryHandle == IntPtr.Zero || vsnprintf == null)
                {
                    throw new JniException("ExtractVfprintf cannot be called now.");
                }

                // Adapted from http://perfec.to/vsnprintf/
                const int FIRSTSIZE = 256; // can use a small number, such as 1, to test growing
                const int MAXSIZE = 256 * 256;
                int bufsize = 0;
                StringBuilder buf = null;
                StringBuilder newbuf = null;
                int nextsize = 0;
                int outsize = 0;

                for (; ; )
                {
                    if (bufsize == 0)
                    {
                        buf = new StringBuilder(FIRSTSIZE);
                        bufsize = FIRSTSIZE;
                    }
                    else if (bufsize >= MAXSIZE)
                    {
                        int success = -1;
                        string message = JniResourceManager.Instance.GetString("jniVsprintfError") +
                            success.ToString(CultureInfo.InvariantCulture);
                        throw new JniException(message);
                    }
                    else
                    {
                        newbuf = new StringBuilder(nextsize);
                        buf = newbuf;
                        bufsize = nextsize;
                    }

                    outsize = vsnprintf(buf, bufsize, format, arguments);

                    if (outsize == -1)
                    {
                        /* Clear indication that output was truncated, but no
                         * clear indication of how big buffer needs to be, so
                         * simply double existing buffer size for next time.
                         */
                        nextsize = bufsize * 2;

                    }
                    else if (outsize == bufsize)
                    {
                        /* Output was truncated (since at least the \0 could
                         * not fit), but no indication of how big the buffer
                         * needs to be, so just double existing buffer size
                         * for next time.
                         */
                        nextsize = bufsize * 2;

                    }
                    else if (outsize > bufsize)
                    {
                        /* Output was truncated, but we were told exactly how
                         * big the buffer needs to be next time. Add two chars
                         * to the returned size. One for the \0, and one to
                         * prevent ambiguity in the next case below.
                         */
                        nextsize = outsize + 2;

                    }
                    else if (outsize == bufsize - 1)
                    {
                        /* This is ambiguous. May mean that the output string
                         * exactly fits, but on some systems the output string
                         * may have been trucated. We can't tell.
                         * Just double the buffer size for next time.
                         */
                        nextsize = bufsize * 2;

                    }
                    else
                    {
                        /* Output was not truncated */
                        break;
                    }
                }
                return buf.ToString();
            }
        }
    }
}
